# Класс и экземпляр класса

## Трансформация от функционального мышления к ОО-мышлению


### Исходное ТЗ

Программа: чтение с клавиатуры; 
вывод в терминал.



```python
def process():
  msg = input('Введите что-нибудь: ')
  print(msg)

process()
```

### Изменения-1 в ТЗ

Добавить: чтение из файла

```python
def file_read():
    return "Зачитали файл"


def process(from_file=False):
    if from_file:
        msg = file_read()
    else:  
        msg = input('Введите что-нибудь: ')
    print(msg)

process()
process(True)
```

### Изменения-2 в ТЗ
Добавлить: отправить данные по сети.

```python
def file_read():
    return "Зачитали файл"

def from_terminal():
    return input('Введите что-нибудь: ')

def to_terminal(msg):
    print(f'Послали в терминал: {msg}')

def send_message(msg):
    print(f"Послали по сети: {msg}")

def process(from_file=False, send_to=False):
    if from_file:
        msg = file_read()
    else:
        msg = from_terminal()
        if send_to:
            send_message(msg)
        else:
            to_terminal(msg)

process()
process(True)
process(True, True)
process(False, True)
```

#### ВЫВОДЫ:

* Грязно
* Много условных выражений
* Сложно поддерживать
* Сложно понять


### Вывод – нас спасет ООП

<details>
<summary>...</summary>
Вокруг нас все объекты – человек, машина, здания, дома, компьютеры.

И объекты между собой общаются, взаимодействуют. 
В программировании общаются между собой только передачей сообщений.

В общем базовая концепция ООП состоит из двух пунктов – всё есть объект и объекты общаются между собой путем передачи сообщений.

Причем Python не 100% ООП-ориентированный язык. 
Если есть объект, то его нужно как-то классифицировать, как-то описать.

Обычно это сравнивают с чертежом, чтобы построить дом надо сделать чертеж.

Или если вспомнить зоологию, корова – копытные есть — значит класс парнокопытные, класс млекопитающих парнокопытные.

</details>

И в ООП такие же классы

```python
class ИмяКласса:
    pass

type(ИмяКласса)
```

Мы видим результат `<class 'type'>`. 

На самом деле класс – это объект для описания других объектов

В блоке `ИмяКласса` мы и будем описывать атрибутику, поведение, методы. 

Вообще всё, что он из себя представляет. 

Пока он пустой, но мы уже можем создавать объекты этого класс. 
Конвеницально принято называть имена классов с большой буквы.
Обращение также как как функции, но не забываем про большую букву


``` python
объект = ИмяКласса()
print (type(объект))
```

Запустим 

> `<class '__main__.ИмяКласса'>`

Как проверить, является ли наш объект экземплярам конкретного класса

``` python
isinstance(объект, ИмяКласса)
```
это функция – предикат, возвращает булев тип. 

Ну и соответственно возвращает 

``` python
print (isinstance(объект, list))
```

### Пример

Предположим, мы хотим определить класс для хранения координат точек на плоскости. 

Для начала я его запишу без какого-либо содержимого, только имя класса Point и все:

```python
class Point:
    pass
```

Здесь оператор pass указывает, что мы в классе ничего не определяем. 

Также обратите внимание, что  в соответствии со стандартом PEP8 имя класса принято записывать с заглавной буквы. 

И, конечно же, называть так, чтобы оно отражало суть этого класса. В дальнейшем мы будем придерживаться этого правила.

Итак, у нас получилось простейшее определение класса с именем Point. Но в таком виде он не особо полезен. 

Пропишем в нем два атрибута: color – цвет точек; circle – радиус точек:

```python
class Point:
    color = 'red'
    circle = 2
```

Обратите внимание, переменные внутри класса обычно называются атрибутами класса или его свойствами. 

Теперь в нашем классе есть два атрибута color и circle. Но, как правильно воспринимать эту конструкцию? 

Фактически, сам класс образует пространство имен, в данном случае с именем Point, в котором находятся две переменные color и circle. И мы можем обращаться к ним, используя синтаксис для пространства имен, например:

```python
Point.color = 'black'
```

или для считывания значения:

```python
Point.circle
```
(В консольном режиме увидим значение 2). А чтобы увидеть все атрибуты класса можно обратиться к специальной коллекции `__dict__`:

```python
Point.__dict__
```

Здесь отображается множество служебных встроенных атрибутов и среди них есть два наших: `color и circle`.

Теперь сделаем следующий шаг и создадим экземпляры этого класса. 

В нашем случае для создания объекта класса Point достаточно после его имени прописать круглые скобки:

```python
a = Point()
```
Смотрите, справа на панели в Python Console у нас появилась переменная a, через которую доступны два атрибута класса: `color и circle`.

Давайте создадим еще один объект этого класса:

```python
b = Point()
```
Появилась переменная b, которая ссылается на новый объект (он расположен по другому адресу) и в этом объекте мы также видим два атрибута класса `Point`.

По аналогии можно создавать произвольное количество экземпляров класса.

С помощью функции type мы можем посмотреть тип данных для переменных a или b:

```python
type(a)
```
Видим, что это класс `Point`. Эту принадлежность можно проверить, например, так:

```python
type(a) == Point
```
или так:

```python
isinstance(a, Point)
```

![](img/simple-class.png)

Во-первых, объекты `a и b` образуют свое пространство имен – пространство имен экземпляров класса и, во-вторых, не содержат никаких собственных атрибутов. 

Свойства `color и circle` принадлежат непосредственно классу `Point` и находятся в нем, а объекты `a и b` лишь имеют ссылки на эти атрибуты класса. 

Поэтому их не случайно называют именно атрибутами класса, подчеркивая этот факт. 

То есть, атрибуты класса – общие для всех его экземпляров. 

И мы можем легко в этом убедиться.

Давайте изменим значение свойства `circle на 1`:
```python
Point.circle = 1
```
И в обоих объектах это свойство стало равно 1. Мало того, если посмотреть коллекцию `__dict__ `у объектов:

```python
a.__dict__
```
то она будет пустой, так как в наших экземплярах отсутствуют какие-либо атрибуты. Но, тем не менее, мы можем через них обращаться к атрибутам класса:

```python
a.color
b.circle
```
Но, если мы выполним присваивание, например:

```python
a.color = 'green'
```
То, смотрите, в объекте a свойство color стало 'green', а в b – прежнее. Почему? Дело в том, что мы здесь через переменную a обращаемся к пространству имен уже экземпляра класса и оператор присваивания в Python создает новую переменную, если она отсутствует в текущей локальной области видимости, то есть, создается атрибут color уже непосредственно в объекте a:


![](img/__dict__.png)


То есть, мы с вами создали локальное свойство в объекте `a`. 

Этот момент нужно очень хорошо знать и понимать. На этом принципе в Python построено формирование атрибутов классов и локальных атрибутов их экземпляров.

Добавление и удаление атрибутов класса
Кстати, по аналогии, мы можем создавать новые атрибуты и в классе, например, так:

```
Point.type_pt = 'disc'
```
Или то же самое можно сделать с помощью специальной функции:

```python
setattr(Point, 'prop', 1)
```
Она создает новый атрибут в указанном пространстве имен (в данном случае в классе Point) с заданным значением. Если эту функцию применить к уже существующему атрибуту:

```python
setattr(Point, 'type_pt', 'square')
```
то оно будет изменено на новое значение.

Если же мы хотим прочитать какое-либо значение атрибута, то достаточно обратиться к нему. В консольном режиме это выглядит так:

```python
Point.circle
```
Но, при обращении к несуществующему атрибуту класса, например:

```python
Point.a
```
возникнет ошибка. Этого можно избежать, если воспользоваться специальной встроенной функцией:

```python
getattr(Point, 'a', False)
```
Здесь третий аргумент – возвращаемое значение, если атрибут не будет найден. Эту же функцию можно вызвать и с двумя аргументами:

```python
getattr(Point, 'a')
```
Но тогда также будет сгенерирована ошибка при отсутствии указанного атрибута. Иначе:

```python
getattr(Point, 'color')
```
она возвратит его значение. 

То есть, эта функция дает нам больше гибкости при обращении к атрибутам класса. 
Хотя на практике ей пользуются только в том случае, если есть опасность обращения к несуществующим атрибутам. 
Обычно, все же, применяют обычный синтаксис:

```python
Point.color
```
Наконец, мы можем удалять любые атрибуты из класса. Сделать это можно, по крайней мере, двумя способами. Первый – это воспользоваться оператором `del`:

```python
del Point.prop
```
Если повторить эту команду и попытаться удалить несуществующий атрибут, возникнет ошибка. Поэтому перед удалением рекомендуется проверять существование удаляемого свойства. Делается это с помощью функции `hasattr`:

```python
hasattr(Point, 'prop')
```
Она возвращает True, если атрибут найден и `False` – в противном случае.

Также удалить атрибут можно с помощью функции:

```python
delattr(Point, 'type_pt')
```
Она работает аналогично оператору `del`.

И, обратите внимание, удаление атрибутов выполняется только в текущем пространстве имен. Например, если попытаться удалить свойство color из объекта b:

```python
del b.color
```

то получим ошибку, т.к. в объекте b не своих локальных свойств и удалять здесь в общем то нечего. 

А вот в объекте a есть свое свойство color, которое мы с вами добавляли:

```python
a.__dict__
```

и его можно удалить:
```python
del a.color
```

Смотрите, после удаления локального свойства color в объекте a становится доступным атрибут `color` класса `Point` с другим значение `black`.

И это логично, т.к. если свойство не обнаруживается в локальной области, то поиск продолжается в следующей (внешней) области видимости. 

А это (для объекта a) класс `Point`. 

Вот этот момент также следует хорошо понимать при работе с локальными свойствами объектов и атрибутами класса.

### Атрибуты экземпляров классов

Теперь, когда мы знаем, как создаются атрибуты, вернемся к нашей задаче формирования объектов точек на плоскости. 

Мы полагаем, что атрибуты `color` и `circle` класса `Point` – это общие данные для всех объектов этого класса. А вот координаты точек должны принадлежать его экземплярам. 

Поэтому для объектов a и b мы определим локальные свойства x и y:
```python
a.x = 1
a.y = 2
b.x = 10
b.y = 20
```

То есть, свойства `x`, `y` будут существовать непосредственно в объектах, но не в самом классе `Point`:

![](img/class-attributs.png)


В результате, каждый объект представляет точку с независимыми координатами на плоскости. 

А цвет и их размер – общие данные для всех объектов.

В заключение этого занятия отмечу, что в любом классе языка Python мы можем прописывать его описание в виде начальной строки, например, так:

```python
class Point:
    "Класс для представления координат точек на плоскости"
    color = 'red'
    circle = 2
```
В результате, специальная переменная:

```python
Point.__doc__
```

будет ссылаться на это описание. 

Обычно, при создании больших программ, в ключевых классах создают такие описания, чтобы в последующем было удобнее возвращаться к ранее написанному коду, корректировать его и использовать, не обращаясь к специальной документации.

### Заключение

Итак, из этого материала вы должны себе хорошо представлять, как определяются классы в Python и создаются объекты класса. 

Что из себя представляют атрибуты класса и атрибуты объектов, как они связаны между собой.

Уметь обращаться к этим атрибутам, добавлять, удалять их, а также проверять существование конкретного свойства в классе или объекте класса.

### Что такое класс?

- Пакет функций
- Главный инструмент ООП
- Оснащение, отражающее реальные объекты в программе
- Фабрика производства экземпляров

<details>
<summary>...</summary>
классы предназначены для создания и управления новыми объектами и поддерживают наследование — механизм настройки и многократного применения кода
</details>



### 3. Принципы (аспекты) ООП

- Наследование
- Композиция

### Механизм ООП в Python - 2 порции магии

- Первый аргумент self
- Поиск в иерархии наследования

## Резюме

- Всё остальное в ООП сводится к функциям
- Не являясь радикально новым, ООП добавляет дополнительный уровень структуры, которая поддерживает
  более эффективное программирование, чем обычные процедурные модели.
- Наряду с рассмотренными ранее функциональными инструментами ООП олицетворяет собой значительный шаг в сторону абстрагирования от компьютерного оборудования, который помогает нам строить более сложно устроенные программы.

## Продолжение введения

- ООП в Python гораздо проще для понимания и использования, чем в других языках, таких как C++ или Java.
- Будучи динамически типизированным языком написания сценариев, Python устраняет большую часть синтаксического беспорядка и сложности, которые затуманивают ООП в других инструментах.
- На самом деле большинство истории ООП в Python сводится до следующего выражения:

```python
объект.атрибут
```

**Классы**

Служат фабриками экземпляров. Атрибуты классов обеспечивают поведение (данные и функции), которое наследуется всеми экземплярами, сгенерированными из них (например, функция для расчета заработной платы сотрудника на основе оклада и отработанных часов).

**Экземпляры**

Представляют конкретные элементы в предметной области программы.
Атрибуты экземпляров хранят данные, которые варьируются для каждого отдельного объекта (скажем, номер карточки социального страхования сотрудника).

```
С точки зрения деревьев поиска экземпляр наследует атрибуты от своего класса, а класс наследует атрибуты от всех классов выше в дереве.
```

- Хотя формально классы и экземпляры, помещаемые в деревья наследования, являются разными типами объектов в модели Python, они практически идентичны;

- Главная цель каждого типа заключается в том, чтобы служить еще одним видом пространства имен — пакета переменных и места, куда мы можем присоединять атрибуты.

- Таким образом, если классы и экземпляры выглядят подобными модулям, то так и должно быть;

- Но, тем не менее, объекты в деревьях классов также имеют автоматически просматриваемые ссылки на другие объекты пространств имен и классы соответствуют операторам, а не целым файлам.

## Резюме

- Основное отличие между классами и экземплярами состоит в том, что классы представляют собой своего рода фабрики ддя генерирования экземпляров.

- Второе отличие между классами и модулями — мы можем иметь только один экземпляр отдельного модуля в памяти (именно потому модуль приходится перезагружать, чтобы получить его новый код), но для классов допускается создавать столько экземпляров, сколько нужно.







